<?php


namespace GordenSong\MySQL;


use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Event\SchemaColumnDefinitionEventArgs;
use Doctrine\DBAL\Events;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MariaDb1027Platform;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Types\Type;
use ReflectionProperty;
use Webmozart\Assert\Assert;

class MySqlSchemaManager extends \Doctrine\DBAL\Schema\MySqlSchemaManager
{
	public function __construct(Connection $conn, ?AbstractPlatform $platform = null)
	{
		parent::__construct($conn, $platform);
	}

	public static function fromDBALMySqlSchemaManager(\Doctrine\DBAL\Schema\MySqlSchemaManager $schemaManager): MySqlSchemaManager
	{
		$reflectionClass = new \ReflectionClass($schemaManager);
		$properties = $reflectionClass->getProperties(ReflectionProperty::IS_PROTECTED);
		foreach ($properties as $property) {
			$property->setAccessible(true);;
			if ($property->getName() == '_conn') {
				$conn = $property->getValue($schemaManager);
			}
			if ($property->getName() == '_platform') {
				$platform = $property->getValue($schemaManager);
			}
		}

		Assert::notNull($conn);
		Assert::notNull($platform);

		return new self($conn, $platform);
	}

	/**
	 * Independent of the database the keys of the column list result are lowercased.
	 *
	 * The name of the created column instance however is kept in its case.
	 *
	 * @param string    $table        The name of the table.
	 * @param string    $database
	 * @param mixed[][] $tableColumns
	 *
	 * @return Column[]
	 */
	protected function _getPortableTableColumnList($table, $database, $tableColumns)
	{
		$eventManager = $this->_platform->getEventManager();

		$list = [];
		foreach ($tableColumns as $tableColumn) {
			$column           = null;
			$defaultPrevented = false;

			if ($eventManager !== null && $eventManager->hasListeners(Events::onSchemaColumnDefinition)) {
				$eventArgs = new SchemaColumnDefinitionEventArgs($tableColumn, $table, $database, $this->_conn);
				$eventManager->dispatchEvent(Events::onSchemaColumnDefinition, $eventArgs);

				$defaultPrevented = $eventArgs->isDefaultPrevented();
				$column           = $eventArgs->getColumn();
			}

			if (! $defaultPrevented) {
				$column = $this->_getPortableTableColumnDefinition($tableColumn);
			}

			if (! $column) {
				continue;
			}

			$column->setCustomSchemaOption('schema', $tableColumn);

			$name        = strtolower($column->getQuotedName($this->_platform));
			$list[$name] = $column;
		}

		return $list;
	}

	/**
	 * {@inheritdoc}
	 */
	protected function _getPortableTableColumnDefinition($tableColumn)
	{
		$tableColumn = array_change_key_case($tableColumn, CASE_LOWER);

		$dbType = strtolower($tableColumn['type']);
		$dbType = strtok($dbType, '(), ');
		assert(is_string($dbType));

		$length = $tableColumn['length'] ?? strtok('(), ');

		$fixed = null;

		if (!isset($tableColumn['name'])) {
			$tableColumn['name'] = '';
		}

		$scale = null;
		$precision = null;
		if (in_array($dbType, ['set', 'enum'])) {
			$type = 'string';
			$array = [];
		} else {
			$type = $this->_platform->getDoctrineTypeMapping($dbType);
			$array = null;
		}

		// In cases where not connected to a database DESCRIBE $table does not return 'Comment'
		if (isset($tableColumn['comment'])) {
			$type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
			$tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
		}

		switch ($dbType) {
			case 'char':
			case 'binary':
				$fixed = true;
				break;

			case 'float':
			case 'double':
			case 'real':
			case 'numeric':
			case 'decimal':
				if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['type'], $match)) {
					$precision = $match[1];
					$scale = $match[2];
					$length = null;
				}

				break;

			case 'tinytext':
				$length = MySqlPlatform::LENGTH_LIMIT_TINYTEXT;
				break;

			case 'text':
				$length = MySqlPlatform::LENGTH_LIMIT_TEXT;
				break;

			case 'mediumtext':
				$length = MySqlPlatform::LENGTH_LIMIT_MEDIUMTEXT;
				break;

			case 'tinyblob':
				$length = MySqlPlatform::LENGTH_LIMIT_TINYBLOB;
				break;

			case 'blob':
				$length = MySqlPlatform::LENGTH_LIMIT_BLOB;
				break;

			case 'mediumblob':
				$length = MySqlPlatform::LENGTH_LIMIT_MEDIUMBLOB;
				break;

			case 'tinyint':
			case 'smallint':
			case 'mediumint':
			case 'int':
			case 'integer':
			case 'bigint':
			case 'year':
				$length = null;
				break;
		}

		if ($this->_platform instanceof MariaDb1027Platform) {
			$columnDefault = $this->getMariaDb1027ColumnDefault($this->_platform, $tableColumn['default']);
		} else {
			$columnDefault = $tableColumn['default'];
		}

		$options = [
			'length' => $length !== null ? (int)$length : null,
			'unsigned' => strpos($tableColumn['type'], 'unsigned') !== false,
			'fixed' => (bool)$fixed,
			'default' => $columnDefault,
			'notnull' => $tableColumn['null'] !== 'YES',
			'scale' => null,
			'precision' => null,
			'autoincrement' => strpos($tableColumn['extra'], 'auto_increment') !== false,
			'comment' => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
				? $tableColumn['comment']
				: null,
		];

		if ($scale !== null && $precision !== null) {
			$options['scale'] = (int)$scale;
			$options['precision'] = (int)$precision;
		}

		$column = new Column($tableColumn['field'], Type::getType($type), $options);

		if (isset($tableColumn['characterset'])) {
			$column->setPlatformOption('charset', $tableColumn['characterset']);
		}

		if (isset($tableColumn['collation'])) {
			$column->setPlatformOption('collation', $tableColumn['collation']);
		}
		if (isset($array)) {
			$column->setCustomSchemaOption('array', $array);
		}

		return $column;
	}

	/**
	 * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers.
	 *
	 * - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted
	 *   to distinguish them from expressions (see MDEV-10134).
	 * - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema
	 *   as current_timestamp(), currdate(), currtime()
	 * - Quoted 'NULL' is not enforced by Maria, it is technically possible to have
	 *   null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053)
	 * - \' is always stored as '' in information_schema (normalized)
	 *
	 * @link https://mariadb.com/kb/en/library/information-schema-columns-table/
	 * @link https://jira.mariadb.org/browse/MDEV-13132
	 *
	 * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7
	 */
	private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?string $columnDefault): ?string
	{
		if ($columnDefault === 'NULL' || $columnDefault === null) {
			return null;
		}

		if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches)) {
			return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES);
		}

		switch ($columnDefault) {
			case 'current_timestamp()':
				return $platform->getCurrentTimestampSQL();

			case 'curdate()':
				return $platform->getCurrentDateSQL();

			case 'curtime()':
				return $platform->getCurrentTimeSQL();
		}

		return $columnDefault;
	}

	/**
	 * @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences
	 */
	private const MARIADB_ESCAPE_SEQUENCES = [
		'\\0' => "\0",
		"\\'" => "'",
		'\\"' => '"',
		'\\b' => "\b",
		'\\n' => "\n",
		'\\r' => "\r",
		'\\t' => "\t",
		'\\Z' => "\x1a",
		'\\\\' => '\\',
		'\\%' => '%',
		'\\_' => '_',

		// Internally, MariaDB escapes single quotes using the standard syntax
		"''" => "'",
	];
}
